/**
 * 
 */
package ai.human

import ai.AIEngine
import data.Player
import data.Position
import data.Data.Role
import data.Data.TeamID
import engine.ActionManager
import engine.Pitch



/**
 * Pass action knowledge base.
 * 
 * @author Tomasz Dąbrowski
 *
 */
class PassKnowBase {

	/**
	 * Instances.
	 */
	static Map instances = [:]
	/**
	 * Touchdown reward modificator.
	 */
	static final float TOUCHDOWN_AWARD = 10.0f

	/**
	 * AI engine.
	 */
	AIEngine aiEngine
	/**
	 * Team id.
	 */
	TeamID teamId
	/**
	 * Last unawarded action. 
	 */
	List lastAction
	/**
	 * Base - states and awards.
	 */
	List base = []
	/**
	 * Awards count.
	 */
	int awardCnt = 0
	/**
	 * Last 100 awards.
	 */
	List awards = []
	/**
	 * Last 100 pass awards.
	 */
	List passAwards = []
	/**
	 * Log file.
	 */
	File log
	/**
	 * Action log.
	 */
	File actionLog
	/**
	 * 
	 */
	long lastStatTime = System.nanoTime()


	/**
	 * Creates and gets an instance.
	 *
	 * @param teamId team id
	 * @return instance
	 *
	 * @author Tomasz Dąbrowski
	 */
	public static PassKnowBase getInstance(TeamID teamId, AIEngine aiEngine = null) {
		PassKnowBase knowBase = instances[teamId]
		if(!knowBase) {
			if(!aiEngine) {
				return null
			}
			knowBase = new PassKnowBase(teamId)
			File f = new File("pass_base.dat")
			if(f.exists()) {
				f.withObjectInputStream { input ->
					input.eachObject {
						def obj = it.find { it.key == teamId.value() }
						if(obj) {
							System.out.println("Pass KBase loaded.");
							knowBase.base = obj.value[0]
							knowBase.awardCnt = obj.value[1]
							knowBase.awards = obj.value[2]
							knowBase.passAwards = obj.value[3]
						}
					}
				}
			}
			knowBase.aiEngine = aiEngine
			instances[teamId] = knowBase
		}
		knowBase
	}

	/**
	 * Creates a new base. 
	 *
	 * @param teamId team id
	 *
	 * @author Tomasz Dąbrowski
	 */
	public PassKnowBase(TeamID teamId) {
		this.teamId = teamId
		log = new File("log${teamId.value()}.txt")
		actionLog = new File("action_log${teamId.value()}.txt")
	}

	/**
	 * Saves all bases.
	 *
	 *
	 * @author Tomasz Dąbrowski
	 */
	public static void saveInstances() {
		instances.values().each  { it.saveDatabase() }
	}


	/**
	 * Saves this base.
	 *
	 *
	 * @author Tomasz Dąbrowski
	 */
	private synchronized void saveDatabase() {
		File f = new File("pass_base.dat")
		f.delete()
		f.withObjectOutputStream { out ->
			out << [(teamId.value()) : [base, awardCnt, awards, passAwards] ]
		}
		f = new File("pass_base_human_${teamId.value()}.txt")
		f.delete()
		base.each {
			it = it.flatten().join(";").replace('.', ',')
			f.append("${it}\r\n")
		}
	}

	/**
	 * Reads the base and gets an action.
	 *
	 * @param srcPlayer passing player
	 * @param dstPlayers players to be passed to
	 * @return player to be passed to or the passing player
	 *
	 * @author Tomasz Dąbrowski
	 */
	public synchronized Player getAction(Player srcPlayer, List dstPlayers) {
		commitKB()
		assert srcPlayer
		assert dstPlayers
		assert !aiEngine.team.hasThrown()

		List currentPlayerAndState = [
			[
				srcPlayer,
				[
					srcPlayer.currentMA() > 0,
					srcPlayer.role() == Role.THROWER,
					calcEndZoneDistance(srcPlayer),
					'-'									
				]
			]
		]
		List playersAndstates = dstPlayers.findAll {
			srcPlayer.distance(it) <= ActionManager.RULER_LONG_BOMB
		}.collect { Player p ->
			[
				p,
				[
					p.currentMA() > 0,
					p.role() == Role.CATCHER,
					calcEndZoneDistance(srcPlayer, p),
					calcPassDistance(srcPlayer, p)										
				]
			]
		}

		(playersAndstates + currentPlayerAndState).collect {
			[
				currentPlayerAndState[0][1],
				it[1],
				0f,
				1,
				0
			]
		}.findAll { playerState ->
			List state = base.find { baseState ->
				baseState[0] == playerState[0] && baseState[1] == playerState[1]
			}
			state == null
		}.each { base.add(it) }
		
		if(!base.isEmpty()) {
			if(base.size() != base.collect { [it[0], it[1]] }.size()) {
				throw new IllegalStateException("duplicated states!")
			}
		}

		List reachStates = base.findAll { baseState ->
			baseState[0] == currentPlayerAndState[0][1] && playersAndstates.find {
				it[1] == baseState[1]
			}
		}
		List choosenPlayerState = reachStates.groupBy { it[2] }.sort { it.key }.max { it.key }.value
		Collections.shuffle(choosenPlayerState)
		choosenPlayerState = choosenPlayerState[0]
		List choosenCurrentState = base.find { baseState ->
			baseState[0] == currentPlayerAndState[0][1] && baseState[1] == currentPlayerAndState[0][1]
		}

		assert choosenPlayerState
		assert choosenCurrentState

		Position ballPos = aiEngine.ballPosition
		assert ballPos

		assert !lastAction

		def noPass = {
			lastAction = [
				srcPlayer,
				srcPlayer.currentMA(),
				choosenCurrentState,
				ballPos
			]
		}

		def pass = {
			Player p = playersAndstates.find { it[1] == choosenPlayerState[1] }[0]
			lastAction = [
				p,
				p.currentMA(),
				choosenPlayerState,
				ballPos
			]
		}

		List states = reachStates + [choosenCurrentState]
		//states.each { it[2] *= 0.9f }

		if(Math.random() > 0.99f) {
			//System.out.println("RANDOM: $states");
			Collections.shuffle(states)
			lastAction = states[0]
		}

		if(lastAction && lastAction == choosenPlayerState) {
			lastAction = [
				srcPlayer,
				srcPlayer.currentMA(),
				lastAction,
				ballPos
			]
		} else if (lastAction) {
			Player p = playersAndstates.find { it[1] == choosenPlayerState[1] }[0]
			lastAction = [
				p,
				p.currentMA(),
				lastAction,
				ballPos
			]
		} else if(choosenPlayerState[2] > choosenCurrentState[2]) {
			pass()
		} else if(choosenPlayerState[2] == choosenCurrentState[2]){
			(Math.random() > 0.5f) ? noPass() : pass()
		} else {
			noPass()
		}
		//System.out.println("Choosen action = $lastAction, pass = ${lastAction[0] != srcPlayer}, random = $random");

		lastAction[2][4]++
		lastAction[0]
	}

	/**
	 * Calcs the pass distance.
	 *
	 * @param srcPlayer passing player
	 * @param dstPlayer player to be passed to
	 * @return distance
	 *
	 * @author Tomasz Dąbrowski
	 */
	public int calcPassDistance(Player srcPlayer, Player dstPlayer) {
		[
			(srcPlayer.x() - dstPlayer.x()).abs(),
			(srcPlayer.y() - dstPlayer.y()).abs()
		].max().collect {
			switch(it) {
				case 0..ActionManager.RULER_QUICK_PASS:
					return 1
				case ActionManager.RULER_QUICK_PASS..ActionManager.RULER_SHORT_PASS:
					return 2
				case ActionManager.RULER_SHORT_PASS..ActionManager.RULER_LONG_PASS:
					return 3
				case ActionManager.RULER_LONG_PASS..ActionManager.RULER_LONG_BOMB:
					return 4
				default:
					assert false
			}
		}.first()
	}

	/**
	 * Calcs a players distance or end zone distance if second parameter is not passed.
	 *
	 * @param p1 passing player
	 * @param p2 player to be passed to (optional)
	 * @return distance distance
	 *
	 * @author Tomasz Dąbrowski
	 */
	public int calcEndZoneDistance(Position p1, Position p2 = null) {
		if(!p1) {
			if( (aiEngine.team.id == TeamID.TEAM_A ? (Pitch.PITCH_WIDTH - 1 - p1.x()) : p1.x()) > 8) {
				return 0
			} else {
				return 1
			}
		}
		int p1Dist = (aiEngine.team.id == TeamID.TEAM_A ? (Pitch.PITCH_WIDTH - 1 - p1.x()) : p1.x())
		p2 = (p2 ?: (aiEngine.team.id == TeamID.TEAM_A ? new Position(Pitch.PITCH_WIDTH - 1, 0) : new Position(0, 0)))
		int p2Dist = (aiEngine.team.id == TeamID.TEAM_A ? (Pitch.PITCH_WIDTH - 1 - p2.x()) : p2.x())
		int dist = (p1Dist - p2Dist)
		switch(dist) {
			case -1..1:
				dist = 0
				break
			//			case -4..4:
			//				dist = (dist > 0) ? 1 : -1
				break
			case -8..8:
				dist = (dist > 0) ? 2 : -2
				break
			default:
				dist = (dist > 0) ? 3 : -3
				break
		}
		dist
	}

	/**
	 * Calcs the danger value.
	 *
	 * @param player player
	 * @return danger value
	 *
	 * @author Tomasz Dąbrowski
	 */
	public int calcDanger(Player player) {
		assert player
		aiEngine.getDangerValue(new Position(player)).collect {
			switch(it) {
				case 0..(AIEngine.NEAR_ENEMY_DANGER_VALUE/2):
					0
					break
				//				case (AIEngine.NEAR_ENEMY_DANGER_VALUE/2)..(AIEngine.NEAR_ENEMY_DANGER_VALUE - 1):
				//					1
				//					break
				default:
					2
					break
			}
		}.first()
	}

	/**
	 * Awards and commits last action to the base.
	 *
	 *
	 * @author Tomasz Dąbrowski
	 */
	public void commitKB(boolean touchDown = false) {
		if(!lastAction) {
			return
		}

		Position ballPos = aiEngine.ballPosition
		Player playerWithBall = lastAction[0].find { it.hasBall() }

		assert !playerWithBall || ballPos

		Position lastBallPos = lastAction[3]
		if(!touchDown && lastBallPos == ballPos && lastAction[0].currentMA() == lastAction[1]) {
			lastAction = null
			return
		}

		float award = (aiEngine.team.id == TeamID.TEAM_A) ? (ballPos.x() - lastBallPos.x()) : (lastBallPos.x() - ballPos.x())
		if(touchDown) {
			award = TOUCHDOWN_AWARD
		} else if(playerWithBall) {
			assert lastBallPos : lastAction
			award = [award, 0].max()
		} else {
			award = 0 - award.abs()
		}

		awardCnt++

		//		base.each { state ->
		//			state[2] *= 0.9f
		//		}

		List state = base.find { state ->
			state[0] == lastAction[2][0] && state[1] == lastAction[2][1]
		}
		assert state : lastAction
		def newAward = state[2] * 0.85f + award
		state[3]++

		actionLog << "${lastAction[0]}".padRight(16)
		actionLog << "MA: ${lastAction[1]}".padRight(6)
		actionLog << "SRC: ${lastAction[2][0]}".padRight(25)
		actionLog << "DST: ${lastAction[2][1]}".padRight(28)
		actionLog << "AVG: ${String.format("%.2f", lastAction[2][2])}".padRight(12)
		actionLog << "CNT: ${lastAction[2][3]}".padRight(10)
		actionLog << "CHN: ${lastAction[2][4]}".padRight(10)
		actionLog << "LPS: ${lastAction[3]}".padRight(14)
		if(lastAction[2][0] != lastAction[2][1]) {
			actionLog.append("PASS, award: $award\r\n")
		} else {
			actionLog.append("MOVE, award: $award\r\n")
		}
		state[2] = newAward

		//		if(awards.size() < 100) {
		//			awards.add(award)
		//		} else {
		//			awards.set(awardCnt % 100, award)
		//		}

		awards.add(award)
		if(awards.size() > 100) {
			awards.remove(0)
		}
		if(lastAction[2][0] != lastAction[2][1]) {
			passAwards.add(award)
			if(passAwards.size() > 100) {
				passAwards.remove(0)
			}
		}

		def avgAward = {
			def v = awards.sum() / awards.size()
			v.toString().replace('.', ',')
		}
		def avgPassAward = {
			def v = passAwards.sum() / passAwards.size()
			v.toString().replace('.', ',')
		}
		if(awardCnt % 10 == 0) {
			log << "$awardCnt;${avgAward()};${avgPassAward()};${base.size()};"
			lastStatTime = (System.nanoTime() - lastStatTime)/10.power(9)
			log << "$lastStatTime;"
			log << "\r\n"
			System.out.println("${awardCnt}:, avg award = ${avgAward()}, avg pass award = ${avgPassAward()}, time = $lastStatTime; kb size = ${base.size()}")
			if(awardCnt % 100 == 0) {
				PassKnowBase.saveInstances()
			}
			lastStatTime = System.nanoTime()
		}
		lastAction = null
	}
}
